package TweetRay;

import Materials.BlackStripeShader;
import Materials.MaterialFactory;
import Materials.MatteShader;
import Materials.ShinyPlasticShader;
import Raytracing.Shaders.Material;
import Raytracing.Shaders.Shader;
import Raytracing.Accelerators.Accelerator;
import Raytracing.Geometry.Triangle;
import java.io.UnsupportedEncodingException;

import processing.core.*;
import SGGUI.*;
import SGCamera.*;
import Raytracing.*;
import TweetRay.Backgrounds.Background;
import TweetRay.Backgrounds.ClearSkyBackground;
import TweetRay.Backgrounds.GradientBackground;
import TweetRay.Backgrounds.ImageBackground;



import Materials.MaterialFactory.Materials;
import TweetRay.Backgrounds.BackgroundFactory;
import TweetRay.Backgrounds.BackgroundFactory.Backgrounds;
import org.apache.commons.codec.binary.Base64;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import java.awt.dnd.*;
import java.awt.datatransfer.*;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.net.URLConnection;
import java.net.URLEncoder;

public class TweetRay extends PApplet implements DropTargetListener {

    SGCamera cam;
    Raytracer tracer;
    static final int NONE = 0;
    static final int MOVE = 1;
    static final int SELECT = 2;
    static final int SCALE = 3;
    static final int ROTATE = 4;
    static final int CAM_ORBIT = 5;
    static final int GRAB_CAM = 6;
    static final int AREA_SELECT = 7;
    static final int X_AXIS = 0;
    static final int Y_AXIS = 1;
    static final int Z_AXIS = 2;
    static final int UNIFORM = 3;
    static final int GLOBAL = 0;
    static final int LOCAL = 1;
    static final int VIEW = 2;
    static final int INTERACTIVE = 0;
    static final int RENDERING = 1;
    static final int RENDERED = 2;
    static final int PAUSED = 5;
    static final int IMPORTING = 6;
    static final int IMPORT_OVERLAY = 7;
    private static final int NUM_BOXES = 25;
    private int mouseMode = NONE;
    private int currentAxis = X_AXIS;
    private int currentCoordinateSystem = GLOBAL;
    private boolean isShiftDown = false;
    private boolean isCtrlDown = false;
    private SGGUI controller;
    private VerticalLayout settings;
    private VerticalLayout materialEditor;
    private VerticalLayout postProcess;
    private SGSlider redSlider, greenSlider, blueSlider, emitSlider, specularSlider, shinySlider;
    private SGSlider lensRadius;
    private SGSlider gamma, alpha, bright;
    private SGButton update, materials, editor, apply;
    private int start, finish, accumulated;
    private int bucketSize = 10;
    private int iterations = 0;
    private int mode = INTERACTIVE;
    private int sizeX = 550;
    private int sizeY = 550;
    private PImage rendered;
    private Film film;
    private List buckets = Collections.synchronizedList(new ArrayList());
    private ArrayList<Triangle> lights = new ArrayList();
    private ArrayList<Entity> entities;
    private ArrayList<Entity> selected = new ArrayList();
    private Material[] shaders;
    private int currentShader;
    private String sceneBuffer = "#☈ 姏燈嗑懯酡榪棠國毯勏縐嵧呱寙牏觯徊喟旝戏呟蔹創吕槫匕緊檒厯嘟牣蟈稰势号芉嗊昀則賓稻玍哧刋潑綹臊杙击成翁笜埑痘姯凐减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减减";
    private int numCPUs = 4;
    PFont fnt;
    private ArrayList<Light> emitters;
    private int timer = 0;
    PImage eMap;
    Background back;
    private boolean fixingBuckets = false;
    int importOverlay = 0;
    Backgrounds background = Backgrounds.SOLID_WHITE;
    //mostly used to allow changes to file format while retaining backwards compat
    private int version = 0;
    private SGSlider focusPoint;
    private SGSlider fieldOfView;
    //Square root of number of samples to cast per pixel per iteration
    private int overSamples = 2;
    private SGSlider groundRad;
    private ThriftySphere gnd;
    private static final int materialPreviewHeight = 30;
    private PVector upperLeft = null;

    public void setup() {

        if (online) {
            sizeX = width;
            sizeY = height;
        }

        size(sizeX, sizeY, P3D);

        //we want this app to support drag and drop...
        DropTarget dt = new DropTarget(this, this);

        frameRate(60);

        fnt = loadFont("writing.vlw");
        textFont(fnt);

        setupGUI();

        cam = new SGCamera(this, 0, 180, 0, 128, 255, 128, SGCamera.UNBOUND, SGCamera.DOLLY);
        cam.setMouseMode(CENTER, SGCamera.ORBIT);
        cam.setRhoLimits(5, 768);
        tracer = new Raytracer(this);



        entities = new ArrayList();
        setupDefaultShaders();

        setupDefaultScene();


        tesselate();

    }

    private void mousePick() {
        System.out.println("picking");
        //clear out the old selection
        if (!isShiftDown) {
            for (Entity bx : entities) {
                bx.setSelected(false);
            }
            selected = new ArrayList();
        }

        Entity b = findNearest();
        if (b != null) {
            if (b.isSelected == false) {
                //select this item
                b.setSelected(true);
                selected.add(b);
                setCurrentShader(b.shaderIndex);

                //if this is part of a group, select the rest of the group
                if (b.getGroup() != 0) {
                    for (Entity e : entities) {
                        if (e.getGroup() == b.getGroup() && e.isSelected == false) {
                            e.setSelected(true);
                            selected.add(e);
                        }
                    }
                }
            }
            updateMaterialEditor(shaders[b.shaderIndex]);
        }
    }

    private void setEarthRad() {
        float earthRad = 750.f * (groundRad.getVal() * groundRad.getVal());
        gnd.setRadius(earthRad);
        gnd.ty = earthRad + 250;
    }

    private void updateMaterialEditor(Material shad) {
        //if something is selected display its material properties

        materialEditor.flush();
        GUIComponent[] controls = shad.getControls();
        if (controls != null) {
            for (GUIComponent GComp : controls) {
                materialEditor.add(GComp);
            }
        }
    }

    private void placeRobot(int group) {

        Material[] robomat = new Material[2];


        setName("Red Robot");

        robomat[0] = MaterialFactory.buildPlastic("Red Plastic", new PVector(.7f, .1f, .1f));
        robomat[1] = MaterialFactory.buildMatte("Yellow Matte", new PVector(.8f, .8f, .1f));

        entities.add(new Box(132, 163, 122, 28.0f, 15.5f, 18.0f, -0, 0, -0, robomat, 0, group));
        entities.add(new Box(100, 203, 115, 7.5f, 17.0f, 9.5f, -0, 0, -0, robomat, 0, group));
        entities.add(new Box(121, 242, 122, 9.5f, 13.0f, 9.5f, -0, 0, -0, robomat, 0, group));
        entities.add(new Box(133, 205, 122, 24.5f, 22.5f, 16.5f, -0, 0, -0, robomat, 0, group));

        entities.add(new Box(165, 203, 122, 7.5f, 17.0f, 9.5f, -0, 0, -0, robomat, 0, group));

        //eyes
        entities.add(new Box(147, 242, 123, 9.5f, 13.0f, 9.50f, -0, 0, -0, robomat, 0, group));
        entities.add(new Box(142, 163, 102, 9.5f, 8.5f, 4.0f, -0, 0, -0, robomat, 1, group));
        entities.add(new Box(119, 162, 101, 9.5f, 8.5f, 4.0f, -0, 0, -0, robomat, 1, group));



    }

    private void placeTrophy(int x, int z, int shader) {
        int height = 25;
        entities.add(new Box(x, 255 - height / 2, z, 40.f, height, 40.f, 0, 0, 0, shaders, 5));
        entities.add(new ThriftySphere(x, 255 - height - height / 2, z, height / 2, shaders, shader));
    }

    private void scaleSelection(int axis) {

        if (axis == 3) {
            System.out.println("UNIFORMS");
            scaleSelection(0);

            //  scaleSelection(1);
            //  scaleSelection(2);

        } else {

            PVector centroid = getSelectedCentroid();

            PVector toward = new PVector();
            PVector cent = new PVector();
            float mag;
            PVector lsy = new PVector();
            for (Entity b : selected) {

                if (axis == 0) {
                    System.out.println("X");
                    lsy = b.getLocalXAxis(false);
                }
                if (axis == 1) {
                    lsy = b.getLocalYAxis(false);
                }
                if (axis == 2) {
                    lsy = b.getLocalZAxis(false);
                }

                float lsyMag = lsy.mag();

                //this will be +/- 1
                float add = (pmouseY - mouseY);

                add *= .05 * lsyMag;

                cent.set(b.tx, b.ty, b.tz);

                toward = PVector.sub(cent, centroid);

                PVector tmp = toward.get();

                float sc = (lsyMag + add) / lsyMag;

                toward.mult(sc);

                toward.sub(tmp);
                if (axis == 0) {
                    toward.y = 0;
                    toward.z = 0;
                } else if (axis == 1) {
                    toward.x = 0;
                    toward.z = 0;
                } else if (axis == 2) {
                    toward.x = 0;
                    toward.y = 0;
                }
                b.translate(toward);
                b.scale(currentAxis, add);

            }
        }
    }

    private void stegEncode(String fileBits, BufferedImage bimage) {
        //steganography time !
        int enc;

        for (int i = 0; i < fileBits.length(); i++) {
            enc = (int) fileBits.codePointAt(i);
            for (int j = 0; j < 16; j++) {

                int readByte = bimage.getRGB(i, j);


                if ((enc & (1 << j)) != 0) {
                    readByte |= 0x00000001;
                } else {
                    readByte &= 0xfffffffe;
                }
                bimage.setRGB(i, j, readByte);

            }
        }
    }

    private void uploadImage() {

        try {

            BufferedImage bimage =
                    new BufferedImage(width, height,
                    BufferedImage.TYPE_INT_RGB);

            bimage.setRGB(0, 0, width, height, buildFinalImage().pixels, 0, width);

            stegEncode(export(), bimage);

            // Creates Byte Array from picture
            ByteArrayOutputStream baos = new ByteArrayOutputStream();


            ImageIO.write(bimage, "png", baos);
            URL url = new URL("http://api.imgur.com/2/upload");

            //encodes picture with Base64 and inserts api key

            String data = URLEncoder.encode("image", "UTF-8") + "=" + URLEncoder.encode(Base64.encodeBase64String(baos.toByteArray()), "UTF-8");
            data += "&" + URLEncoder.encode("key", "UTF-8") + "=" + URLEncoder.encode("fa04d47488485c649b5ae2f4c8c54fe9", "UTF-8");


            System.out.println("building buffers");
            // opens connection and sends data
            URLConnection conn = url.openConnection();
            conn.setDoOutput(true);
            OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());



            System.out.println("writing");

            wr.write(data);
            wr.flush();

            System.out.println("Reading");
            char[] buffer = new char[1024];
            InputStreamReader is = new InputStreamReader(conn.getInputStream());
            //BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());

            int n;
            StringBuffer sb = new StringBuffer();
            while ((n = is.read(buffer)) != -1) {
                sb.append(buffer, 0, n);
            }
            is.close();
            //System.out.println(sb.toString());
            try {
                URI link = null;
                String imgurPage = "";
                String[] tokens = sb.toString().split("[<>]");
                for (int i = 0; i < tokens.length; i++) {

                    if (tokens[i].equals("imgur_page")) {
                        imgurPage = tokens[i + 1];

                        break;
                    }
                }
                link = new URI(imgurPage);
                URI redditLink = new URI("http://www.reddit.com/r/thriftytracer/submit?url=" + imgurPage + "&title=thriftyRender");

                //java.awt.Desktop.getDesktop().browse(link);
                java.awt.Desktop.getDesktop().browse(redditLink);
            } catch (Exception e) {
                System.out.println(e);
            }



        } catch (Exception e) {
            System.out.println(e);
        }
    }

    private void savePNG(PImage img, String path, String fileBits) {


        try {
            BufferedImage bimage =
                    new BufferedImage(width, height,
                    BufferedImage.TYPE_INT_RGB);

            bimage.setRGB(0, 0, width, height, img.pixels, 0, width);
            stegEncode(fileBits, bimage);


            File file = new File(path);
            String extension = path.substring(path.lastIndexOf('.') + 1);

            //new way with metadata
            Iterator<ImageWriter> iterator = ImageIO.getImageWritersBySuffix("png");

            if (!iterator.hasNext()) {
                throw new Error("No image writer for PNG");
            }

            ImageWriter imagewriter = iterator.next();
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            imagewriter.setOutput(ImageIO.createImageOutputStream(bytes));

            IIOImage iioImage = new IIOImage(bimage, null, null);


            FileOutputStream out = new FileOutputStream(file);
            imagewriter.setOutput(ImageIO.createImageOutputStream(out));
            imagewriter.write(iioImage);
            out.close();
            imagewriter.dispose();


        } catch (Exception e) {
            e.printStackTrace();

        }

    }

    private PImage buildFinalImage() {


        PGraphics pg = createGraphics(width, height, JAVA2D);
        pg.smooth();


        pg.beginDraw();
        pg.image(rendered, 0, 0);

        pg.fill(0, 0, 0, 128);
        pg.noStroke();

        int barHeight = 15;

        pg.rect(0, height - barHeight, width, barHeight);

        pg.textFont(fnt);
        Graphics2D pg2 = ((PGraphicsJava2D) pg).g2;

        pg2.setFont(new Font("SansSerif", Font.PLAIN, 11));
        pg2.setColor(Color.white);


        start = millis();
        String time = calcTime();

        pg2.drawString(iterations + " iterations in " + time, 3 * width / 4, height - 4);

        pg.endDraw();

        return pg;



    }

    private void saveRendering() {
        //get location for saved file
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                try {
                    JFileChooser fc = new JFileChooser(new File("c:\\temp\\image.png"));
                    int returnVal = fc.showSaveDialog(null);
                    if (returnVal == JFileChooser.APPROVE_OPTION) {
                        File selFile = fc.getSelectedFile();
                        if (selFile != null) {

                            String outName = selFile.getAbsolutePath();
                            saveRendering(outName);

                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private void saveRendering(String path) {
        //get the bits for the scene for encoding
        String bits = export();

        savePNG(buildFinalImage(), path, bits);

    }

    private void setupDefaultScene() {

        // entities.add(new Box(128,255,128,500,1,500,0,0,0,shaders,1));
        entities.add(new GroundPlane(shaders, 1));
        //placeRobot(1);

        //entities.add(new Box(128,250,128,1,1,1,0,0,0,shaders,0));
        //entities.add(new Box(128,240,128,10,10,10,0,0,0,shaders,1));


        float earthRad = 1000;
        gnd = new ThriftySphere(128, earthRad, 128, earthRad - 250, shaders, 0);
        gnd.tag = Entity.EntityTag.GROUND;
        //sort of circuitous
        setEarthRad();
        //entities.add(gnd);


        back = BackgroundFactory.getBackGround(background, this);


    }

    private void setupGUI() {
        controller = new SGGUI(this, fnt);
        controller.GUI_TEXT = color(255, 255, 255, 255);
        //
        settings = new VerticalLayout(controller, 10, 10, 150);
        settings.title = "Settings";

        lensRadius = new SGSlider("Depth of Field", 0, 1, 0, 7);

        focusPoint = new SGSlider("Focus Point", 0, 1, 0, 7);
        focusPoint.addGUIListener(this);

        fieldOfView = new SGSlider("Field of View", 1, 1, 0, 3);
        fieldOfView.addGUIListener(this);


        groundRad = new SGSlider("Ground Radius", 3, 1, 0, 3);
        groundRad.addGUIListener(this);

        update = new SGButton("Render", this);
        update.addGUIListener(this);


        settings.add(lensRadius);
        settings.add(focusPoint);
        settings.add(fieldOfView);
        settings.add(groundRad);
        settings.add(update);
        controller.add(settings);


        materialEditor = new VerticalLayout(controller, 10, 100, 150);
        redSlider = new SGSlider("Red", 4 / 7f, 1 / 7.f, 0, 1f);
        greenSlider = new SGSlider("Green", 4 / 7f, 1 / 7.f, 0, 1f);
        blueSlider = new SGSlider("Blue", 4 / 7f, 1 / 7.f, 0, 1f);

        redSlider.addGUIListener(this);
        greenSlider.addGUIListener(this);
        blueSlider.addGUIListener(this);

        materialEditor.add(redSlider);
        materialEditor.add(greenSlider);
        materialEditor.add(blueSlider);

        controller.add(materialEditor);


        postProcess = new VerticalLayout(controller, 10, 10, 150);
        gamma = new SGSlider("Gamma", 2.2f, .1f, .1f, 3f);
        alpha = new SGSlider("Alpha", .18f, .01f, .05f, .75f);
        bright = new SGSlider("Bright", 1f, .05f, .01f, 3f);
        apply = new SGButton("Apply", this);


        gamma.addGUIListener(this);
        alpha.addGUIListener(this);
        bright.addGUIListener(this);

        //postProcess.add(gamma);
        postProcess.add(alpha);
        postProcess.add(bright);
        postProcess.add(apply);
        controller.add(postProcess);
    }

    private void setupDefaultShaders() {


        MaterialFactory MF = new MaterialFactory(this);
        shaders = new Material[11];

        for (int i = 0; i < 11; i++) {
            shaders[i] = MaterialFactory.buildMatte("Matte", new PVector(1 - i / 11.f, 1 - i / 11.f, 1 - i / 11.f));
        }

        //shaders[2] = MaterialFactory.getMaterial(Materials.TREE_GENERATOR);

    }

    public void draw() {



        if (mode == INTERACTIVE) {
            settings.setVisible(1);
            materialEditor.setVisible(1);
            postProcess.setVisible(0);


            background(60, 60, 60);
            lights();

            //camera is always looking at the oneth entity (zero is the ground)
            if (entities.size() > 1) {
                Entity tgt = entities.get(1);
                cam.setTarget(tgt.tx, tgt.ty, tgt.tz);
            }
            /*
            //round the bit camera position to 7 bit precision
            int x = (int) cam.pos.x;
            x /= 16;
            x = Math.round(x);
            x *= 16;
            
            float y = cam.pos.y;
            y /= 16;
            y = Math.round(y);
            y *= 16;
            
            float z = cam.pos.z;
            z /= 16;
            z = Math.round(z);
            z *= 16;
            
            cam.setDesiredPosition(x, y, z);
            
            
             * 
             */
            cam.feed();


            noFill();
            stroke(100, 100, 100, 100);

            //draw the environment
            pushMatrix();
            translate(128, 128, 128);
            box(255, 255, 255);
            popMatrix();


            noStroke();


            for (Entity b : entities) {
                b.draw((PGraphics3D) g);
            }


            //tracer.accelerator.draw(this);

            drawManipulators();

            drawDiagnostics();


            //screen space ops here
            pushMatrix();
            perspective();
            camera();

            hint(DISABLE_DEPTH_TEST);
            if (upperLeft != null) {

                fill(128, 128, 64, 50);
                rect(upperLeft.x, upperLeft.y, mouseX - upperLeft.x, mouseY - upperLeft.y);

            }




            /*
            //draw the focus points!
            
            fill(0, 0, 0, 50);
            noStroke();
            rect(width / 2 - 10, height / 2 - 10, 20, 20);
            
            
             * 
             */
            unhint(DISABLE_DEPTH_TEST);
            popMatrix();

            drawMaterialEditor();

            /*
            
            if (importOverlay > 0) {
            if (importOverlay < 256){
            tint(255,255,255,importOverlay);
            image(rendered, 0, 0);
            }
            importOverlay-=2;
            }
             */

        } else if (mode
                == RENDERED
                || mode
                == RENDERING) {
            settings.setVisible(0);
            materialEditor.setVisible(0);
            postProcess.setVisible(0);
            background(rendered);

            timer++;
            if (timer > 10) {
                timer = 0;
                updateCanvas();
            }


            //draw the rendering overlay
            finish = millis();

            int numBuckets = sizeX / bucketSize;
            numBuckets *= numBuckets;

            fill(32, 32, 0, 128);
            noStroke();
            rect(0, height - 20, width, 20);

            //this will draw a progress bar on the current iteration
            rect(0, height - 20, width - (width * buckets.size() / numBuckets), 20);

            String time = calcTime();

            fill(255, 255, 255);
            text("Press 'P' to pause | "
                    + "Iterations: " + iterations
                    + " | Time: " + time,
                    width / 3, height - 5);

        } else if (mode
                == PAUSED) {
            postProcess.setVisible(1);
            //show the paused edit menu
            background(rendered);
            fill(0, 0, 0, 64);
            noStroke();
            rect(0, height - 20, width, 20);
            fill(255, 255, 255);
            text("Paused. Press 'r' to restart.", width / 2, height - 5);


        } else if (mode == IMPORTING) {
            background(0);

        }
    }

    private String calcTime() {

        int secs = (int) ((accumulated + millis() - start) / 1000.f);
        int hours = secs / 3600;
        secs = secs % 3600;

        int mins = secs / 60;
        secs = secs % 60;
        if (hours > 0) {
            return hours + "h " + mins + "m " + secs + "s ";
        } else if (mins > 0) {
            return mins + "m " + secs + "s ";
        } else {
            return secs + "s ";
        }
    }

    private void directLighting(Ray pick, Intersectable tritemp, PVector accum, int ID) {
        int numLightSamples = 2;
        //select a light at random
        int sz = emitters.size();
        int which = (int) (sz * Math.random());
        if (sz > 0) {
            Light lightTri;
            for (int l = 0; l < numLightSamples; l++) {
                lightTri = emitters.get(which);
                PVector lt = lightTri.getSample();


                //direct light
                Ray toLight = new Ray();
                toLight.origin = pick.project(pick.distance);
                toLight.direction = PVector.sub(lt, toLight.origin);
                float r = toLight.direction.mag();
                toLight.direction.div(r);
                toLight.minDist = .001f;
                toLight.maxDist = r - .001f;
                tracer.accelerator.findNearest(toLight, ID);


                //V(x,y) == 1
                if (toLight.occluder == null) {

                    PVector lightAccum = new PVector();

                    float G = toLight.direction.absDot(lightTri.getNormal());

                    PVector outgoing = pick.direction.get();
                    outgoing.mult(-1.f);


                    PVector brdf = tritemp.shader.getReflectance(pick.worldToLocal(toLight.direction), pick.worldToLocal(outgoing), null, pick.tu, pick.tv, null, null);

                    float cosi = Math.abs(pick.normal.dot(toLight.direction));
                    float pdf = (r * r) / (G * lightTri.getArea());

                    lightAccum = new PVector(1f, 1f, 1f);
                    lightTri.getEmit(lightAccum);
                    lightAccum.mult(brdf);

                    lightAccum.mult(cosi / pdf);


                    accum.add(PVector.mult(lightAccum, 1.f / (numLightSamples)));

                }
            }
        }
    }

    private void drawDiagnostics() {
        //fill(255,255,255,200);
        //camera();
        //rect(0,height-20,width,height);
        fill(255);
        textFont(fnt);

        //subtract 1 from the size because the ground is assumed
        text(entities.size() - 1 + "/25 boxes used", width - 150, 20);
        switch (currentCoordinateSystem) {
            case GLOBAL:
                text("Global Coords", width - 150, 35);
                break;
            case VIEW:
                text("View Coords", width - 150, 35);
                break;
            case LOCAL:
                text("Local Coords", width - 150, 35);
                break;
        }
        text("back: " + background, width - 150, 50);
        if (selected.size() == 1) {
            text("Texture: (" + selected.get(0).shaderIndex + ") " + shaders[selected.get(0).shaderIndex].getName(), width - 150, 65);
        }
    }

    private void drawMaterialEditor() {

        //goto screenspace
        pushMatrix();
        perspective();
        camera();
        hint(DISABLE_DEPTH_TEST);

        fill(0, 0, 0);
        rect(0, height - materialPreviewHeight - 1, width, height);

        for (int i = 0; i < shaders.length; i++) {
            if (currentShader == i) {
                fill(255, 255, 0);
                rect(i * width / shaders.length, height - materialPreviewHeight, width / shaders.length, materialPreviewHeight);
            }

            fill(255 * shaders[i].fillColor.x, 255 * shaders[i].fillColor.y, 255 * shaders[i].fillColor.z);
            rect(i * width / shaders.length + 2, height - materialPreviewHeight + 2, width / shaders.length - 4, materialPreviewHeight - 4);

        }
        unhint(DISABLE_DEPTH_TEST);
        popMatrix();

    }

    public void GUIEventPerformed(GUIEvent e) {

        if (e.title.equals("Render")) {
            render();
        } else if (e.title.equals("Edit Material")) {
            key = 'b';
            keyPressed();
        } else if (e.title.equals("Gamma")) {
            film.setGamma(gamma.getVal());

        } else if (e.title.equals("Alpha")) {
            film.setAlpha(alpha.getVal());

        } else if (e.title.equals("Bright")) {
            film.setBright(bright.getVal());

        } else if (e.title.equals("Apply")) {
            updateCanvas();
        } else if (e.title.equals("Focus Point")) {
            System.out.println("adjusting focus point");
        } else if (e.title.equals("Field of View")) {
            cam.setFOV(1.75f * (1.f - fieldOfView.getVal() / 3.f) + .2f);

        } else if (e.title.equals("Ground Radius")) {
            setEarthRad();


        } else {
            //   shaders[currentShader].fillColor.x = redSlider.getVal();
            //   shaders[currentShader].fillColor.y = greenSlider.getVal();
            //   shaders[currentShader].fillColor.z = blueSlider.getVal();
            //   shaders[currentShader].emit = emitSlider.getVal();
            //shaders[currentShader].brdf.setN(shinySlider.getVal());
            //shaders[currentShader].brdf.setKs(specularSlider.getVal());
        }
    }

    /*
    public void mousePressed(){
    System.out.println("mousePressed");
    }
     */
    public void mouseClicked() {
        System.out.println("mouseClicked");
        if (!mouseEvent.isConsumed()) {

            if (mode == RENDERING) {
                System.out.println("Tonemapped to:"
                        + red(film.toneMap(mouseX, mouseY))
                        + green(film.toneMap(mouseX, mouseY))
                        + blue(film.toneMap(mouseX, mouseY)));
            } else {
                if (mouseY > height - materialPreviewHeight) {

                    int which = (int) ((mouseX / (float) width) * shaders.length);

                    setCurrentShader(which);
                    updateMaterialEditor(shaders[which]);
                    for (Entity b : selected) {
                        if (b instanceof Box) {
                            ((Box) b).setShader(which);
                        }
                    }
                } else {

                    if (mouseMode == AREA_SELECT) {

                        if (upperLeft == null) {
                            upperLeft = new PVector(mouseX, mouseY, 0);
                        } else {


                            //correct points in case they picked in a non standard order
                            float tmp;
                            if (upperLeft.x > mouseX) {
                                tmp = upperLeft.x;
                                upperLeft.x = mouseX;
                                mouseX = (int) tmp;
                            }
                            if (upperLeft.y > mouseY) {
                                tmp = upperLeft.y;
                                upperLeft.y = mouseY;
                                mouseY = (int) tmp;
                            }

                            selectArea(upperLeft, new PVector(mouseX, mouseY, 0));
                            upperLeft = null;
                            mouseMode = SELECT;
                        }


                    } else {

                        mousePick();
                    }
                }
            }
        }
    }

    public void mouseDragged() {

        if (mode == RENDERING) {
            //cancel all remaining renderbuckets
            buckets = new ArrayList();
            frameRate(60);
        } else if (mode == RENDERED) {
            frameRate(60);
            mode = INTERACTIVE;
        } else if (!mouseEvent.isConsumed()) {
            if (mouseMode == GRAB_CAM && mouseButton == LEFT) {
                PVector up = PVector.mult(cam.up, -1.f * (mouseY - pmouseY));
                PVector right = PVector.mult(cam.right, -1.f * (mouseX - pmouseX));
                cam.translate(up.x, up.y, up.z);
                cam.translate(right.x, right.y, right.z);

            } else if (!selected.isEmpty()) {
                if (mouseMode == MOVE && mouseButton == LEFT) {

                    //convert pixel drags in screen space to world motion at object distance

                    float ang = cam.fov / width; //radians per pixel of screen space

                    Entity sel = selected.get(0);

                    float dist = PVector.dist(new PVector(sel.tx, sel.ty, sel.tz), cam.pos);

                    float worldDist = (float) Math.sin(ang) * dist;

                    switch (currentCoordinateSystem) {
                        case GLOBAL:
                            PVector dir = new PVector();
                            float amt = 0;
                            switch (currentAxis) {
                                case X_AXIS:
                                    dir = new PVector(-1, 0, 0);
                                    amt = worldDist * (mouseY - pmouseY);
                                    break;
                                case Y_AXIS:
                                    dir = new PVector(0, 1, 0);
                                    amt = worldDist * (mouseY - pmouseY);
                                    break;
                                case Z_AXIS:
                                    dir = new PVector(0, 0, -1);
                                    amt = worldDist * (mouseY - pmouseY);
                                    break;
                            }
                            for (Entity b : selected) {
                                b.translate(PVector.mult(dir, amt));
                            }
                            break;
                        case LOCAL:


                            break;
                        case VIEW:
                            for (Entity b : selected) {
                                b.translate(PVector.mult(cam.right, worldDist * (mouseX - pmouseX)));
                                b.translate(PVector.mult(cam.up, worldDist * (mouseY - pmouseY)));
                            }
                            break;
                    }

                } else if (mouseMode == SCALE && mouseButton == LEFT) {
                    scaleSelection(currentAxis);
                } else if (mouseMode == ROTATE && mouseButton == LEFT) {
                    mouseRotate();
                }
            }

        }
    }

    private void mouseRotate() {

        if (!selected.isEmpty()) {
            //build the rotation exis
            PVector center = getSelectedCentroid();
            PVector dir = center.get();
            switch (currentCoordinateSystem) {
                case GLOBAL:
                    switch (currentAxis) {
                        case X_AXIS:
                            dir.x += 1;
                            break;
                        case Y_AXIS:
                            dir.y += 1;
                            break;
                        case Z_AXIS:
                            dir.z += 1;
                            break;
                    }
                    break;
                case LOCAL:
                    Entity first = selected.get(0);
                    switch (currentAxis) {
                        case X_AXIS:
                            dir = first.getLocalXAxis(true);
                            dir.add(center);
                            break;
                        case Y_AXIS:
                            dir = first.getLocalYAxis(true);
                            dir.add(center);
                            break;
                        case Z_AXIS:
                            dir = first.getLocalZAxis(true);
                            dir.add(center);
                            break;
                    }
                    break;
            }
            for (Entity b : selected) {
                b.rotate(center, dir, (float) (2 * Math.PI / 64.f * (mouseY - pmouseY)));
            }
        }
    }

    //Erase all objects in the scene
    public void reset() {
        entities.clear();

        /*
        float earthRad = 1000;
        gnd = new ThriftySphere(128, earthRad, 128, earthRad - 250, shaders, 0);
        gnd.tag = Entity.EntityTag.GROUND;
        
        entities.add(gnd);
         */
        entities.add(new GroundPlane(shaders, 1));
        tesselate();
    }

    public void render() {

        iterations = -1;

        //don't waste cycles redrawing
        frameRate(4);
        tesselate();

        //get the appropriate autofocus depth from the scene
        //cast a ray down the center of the scene
        Ray ray = new Ray(cam.pos.get(), cam.forward.get());
        tracer.accelerator.findNearest(ray, 0);
        System.out.println("Autofocusing at: " + ray.distance);
        cam.focus = ray.distance;


        mode = RENDERING;

        finish = 0;

        film = new Film(sizeX, sizeY);

        rendered = new PImage(width, height);

        createBuckets();

        continueRender();


    }

    public void continueRender() {
        RenderThread thrd;
        start = millis();

        for (int i = 0; i < numCPUs; i++) {
            thrd = new RenderThread(i);
            thrd.start();
        }
    }

    public void createBuckets() {
        iterations++;
        //create all the renderbuckets
        int numB = sizeX / bucketSize;
        int numBY = sizeY / bucketSize;
        for (int i = 0; i < numB; i++) {
            for (int j = 0; j < numB; j++) {
                buckets.add(new RenderBucket(i * width / numB, i * width / numB + width / numB, j * height / numBY, j * height / numBY + height / numBY));
            }
        }
        Collections.shuffle(buckets);
    }

    private String stegoRead() {

        int read, test;
        int[] bytes = new int[141];
        for (int i = 0; i < 141; i++) {
            read = 0;
            for (int j = 0; j < 16; j++) {
                int bob = rendered.get(i, j);
                test = bob & 0x00000001;
                if (test == 1) {
                    read |= (1 << j);
                }
            }
            bytes[i] = read;
        }
        String readdesc = new String(bytes, 0, 141);
        return readdesc;
    }

    private void tesselate() {

        tracer.accelerator.objects.clear();
        emitters = new ArrayList();


        ArrayList<Entity> geoms = new ArrayList();

        for (Entity b : entities) {
            if (b instanceof Traceable) {
                Material m = b.shaders[b.shaderIndex];
                if (m instanceof GeometryGenerator) {
                    ((GeometryGenerator) b.shaders[b.shaderIndex]).generate((Box) b, geoms, shaders);
                } else {
                    geoms.add(b);
                }

            }

        }
        //tesselate and populate the raytracer
        ArrayList tmp;

        for (Entity b : geoms) {
            //don't tesselate lamps or anything else in there
            if (b instanceof Traceable) {

                //check if this is geometry or a bounding box for 
                //geometry generating material
                Material m = b.shaders[b.shaderIndex];

                if (m instanceof Shader) {

                    tmp = ((Traceable) b).tesselate();

                    for (int i = 0; i < tmp.size(); i++) {
                        Intersectable t = (Intersectable) tmp.get(i);
                        tracer.add(t);
                    }
                }
            }
            if (b instanceof Light) {
                emitters.add((Light) b);
                System.out.println("Got light");
            }
        }

        //tell the acclerator to turn the list of triangles it just got into a
        //tree that cam be used.
        ((Accelerator) tracer.accelerator).validate();



        float gHeight = 25;
        float gWidth = 2;

        PVector root = new PVector(100, 255, 100);

        PVector up = new PVector(random(-1, 1), 1, random(-1, 1));
        up.normalize();

        /*
        for (int i = 0; i < 50000; i++) {
        
        root.x = random(-128, 383);
        root.z = random(-128, 383);
        
        
        
        gHeight = random(10, 10);
        gWidth = random(1f, 1f);
        up.mult(gHeight);
        tracer.add(new Triangle((Shader)shaders[0],
        new PVector(root.x, root.y, root.z),
        new PVector(root.x + gWidth, root.y, root.z),
        new PVector(root.x, root.y - gHeight, root.z),
        0, 0, 0, 0, 0, 0, null));
        tracer.add(new Triangle((Shader)shaders[0],
        new PVector(root.x + gWidth, root.y, root.z),
        new PVector(root.x + gWidth, root.y - gHeight, root.z),
        new PVector(root.x, root.y - gHeight, root.z),
        0, 0, 0, 0, 0, 0, null));
        }
        
        ((Accelerator) tracer.accelerator).validate();
        
         */

    }

    public void keyPressed() {

        cam.setMouseMode(LEFT, SGCamera.UNBOUND);

        if (mode == RENDERING) {

            if (key == 'p') {
                accumulated = accumulated + millis() - start;
                mode = PAUSED;
                frameRate(60);

            } else if (key == 'i') {

                mode = INTERACTIVE;
                frameRate(60);
            }

        } else if (mode == PAUSED) {
            if (key == 'w') {

                if (!online) {
                    saveRendering();
                }

            } else if (key == 'v') {
                if (!online) {
                    uploadImage();
                }
            } else if (key == 'r') {
                mode = RENDERING;
                continueRender();
            }


        } else if (isShiftDown == true) {

            if (keyCode == '1' || keyCode == '2' || keyCode == '3') {
                System.out.println("Setting group");
                for (Entity e : selected) {
                    e.setGroup(Integer.parseInt("" + keyCode));
                }
            }

        } else {

            if (keyCode == SHIFT) {
                isShiftDown = true;
            } else if (keyCode == CONTROL) {
                isCtrlDown = true;
            } else if (key == 'j') {
                loadRender();
            } else if (key == 'b') {
                mouseMode = AREA_SELECT;
            } else if (key == 'e') {
                export();
            } else if (key == 'a') {
                //insert at the end of the list so the camera doesn't grab focus
                //(the camera is always looking at element 0)
                entities.add(entities.size(), new Box(128, 200, 128, 10, 10, 10, 0, 0, 0, shaders, 1));
                tesselate();

            } else if (key == 's') {
                mouseMode = SCALE;
                currentCoordinateSystem = LOCAL;
            } else if (key == 'm') {
                mouseMode = MOVE;
                currentCoordinateSystem = VIEW;

            } else if (key == 'r') {
                mouseMode = ROTATE;
                currentCoordinateSystem = LOCAL;
            } else if (key == 'g') {
                currentCoordinateSystem = GLOBAL;
            } else if (key == 'l') {
                currentCoordinateSystem = LOCAL;
            } else if (key == 'v') {
                currentCoordinateSystem = VIEW;
            } else if (key == 'x') {
                currentAxis = X_AXIS;
            } else if (key == 'y') {
                currentAxis = Y_AXIS;
            } else if (key == 'z') {
                currentAxis = Z_AXIS;
            } else if (key == 'u') {
                currentAxis = UNIFORM;
            } else if (key == 'i') {
                mode = INTERACTIVE;
            } else if (key == 'o') {
                mouseMode = NONE;
                cam.setMouseMode(LEFT, SGCamera.ORBIT);
            } else if (key == 'h') {
                mouseMode = GRAB_CAM;
            } else if (key == 'c') {
                copy();
                mouseMode = MOVE;
                currentAxis = VIEW;
            } else if (key == 't') {
                //camera always tracks item at the 1 position, so put the 
                //first selected item into that position in the entity list

                entities.remove(selected.get(0));
                entities.add(1, selected.get(0));

            } else if (keyCode == DELETE) {
                for (Entity b : selected) {
                    entities.remove(b);
                }
            } else if (key == 'p') {
                loadString(sceneBuffer);
            } else if (key == '2') {
                cam.setDesiredPosition(cam.target.x + 200, cam.target.y, cam.target.z);
            } else if (key == '4') {
                cam.setDesiredPosition(cam.target.x, cam.target.y, cam.target.z + 200);
            } else if (key == '6') {
                cam.setDesiredPosition(cam.target.x, cam.target.y, cam.target.z - 200);
            } else if (key == '8') {
                cam.setDesiredPosition(cam.target.x + .1f, cam.target.y - 200, cam.target.z);
            } else if (key == 'q') {
                reset();
            } else if (key == 'f') {
                for (Entity b : entities) {
                    System.out.print(b.toCode());
                }
            } else if (key == '.') {
                if (currentShader < 11) {
                    currentShader++;
                    for (Entity e : selected) {
                        e.setShader(currentShader);
                    }
                    updateMaterialEditor(shaders[currentShader]);
                }
            } else if (key == ',') {
                if (currentShader > 0) {
                    currentShader--;
                    for (Entity e : selected) {

                        e.setShader(currentShader);
                    }
                    updateMaterialEditor(shaders[currentShader]);
                }
            } else if (key == ']') {
                //current shader gets the next material type

                if (shaders[currentShader].type < 32) {
                    shaders[currentShader] = MaterialFactory.getMaterial(Materials.values()[shaders[currentShader].type + 1]);
                }
                System.out.println("up" + shaders[currentShader].type);
            } else if (key == '[') {
                //current shader gets the next previsou type

                if (shaders[currentShader].type > 0) {
                    shaders[currentShader] = MaterialFactory.getMaterial(Materials.values()[shaders[currentShader].type - 1]);
                }
                System.out.println("down" + shaders[currentShader].type);
            } else if (key == '=') {
                if (background.ordinal() < 32) {
                    background = Backgrounds.values()[background.ordinal() + 1];
                    back = BackgroundFactory.getBackGround(background, this);

                }
            } else if (key == '-') {
                if (background.ordinal() > 0) {
                    background = Backgrounds.values()[background.ordinal() - 1];
                    back = BackgroundFactory.getBackGround(background, this);

                }
            }

        }
    }

    private void copy() {
        ArrayList<Entity> toRet = new ArrayList<Entity>();
        for (Entity b : selected) {
            toRet.add(b.copy());
        }
        System.out.println(entities.size());
        entities.addAll(entities.size(), toRet);
    }

    private String export() {

        UTFWriter output = new UTFWriter();

        //current version. Do this first because it really affects how
        //we're going to parse this thing
        output.writeBits(version, 4);

        /*
        
        //YEAH I NEED THE BITS... FORGET THIS!
        //write the hash sign
        output.writeByte(0x23);
        
        output.writeByte(0x26 << 8 | 0x08);
        output.writeByte(0x20 << 8 | 0x08);
        
         *
         * 
         */
        //TODO camera export
        System.out.println(cam.pos);
        //cam is limited to +-512 in each direction
        //we need to cram that into 24 bits. eek.
        //-512 - 512


        float x = cam.pos.x;
        //0 - 2048
        x += 1024;
        //0-256
        x /= 8;
        //round, then cast so it goes up half the time...
        output.writeBits((int) Math.round(x), 8);

        float y = cam.pos.y;
        y += 1024;
        y /= 8;
        output.writeBits((int) Math.round(y), 8);

        float z = cam.pos.z;
        z += 1024;
        z /= 8;
        output.writeBits((int) Math.round(z), 8);


        //rotation
        //TODO implement this!
        output.writeBits((int) cam.getRoll(), 5);

        //Depth of Field
        //TODO normalize this!
        output.writeBits((int) lensRadius.getVal(), 3);

        //Focus point
        output.writeBits((int) focusPoint.getVal(), 3);

        //Field of view
        output.writeBits((int) fieldOfView.getVal(), 2);


        System.out.println("Exporting ground");
        output.writeBits((int) groundRad.getVal(), 2);

        System.out.println("Exporting background");
        output.writeBits(background.ordinal(), 5);


        System.out.println("Exporting shaders.");

        for (Material m : shaders) {
            ((Exportable) m).pack(output);
        }



        System.out.println("Exporting geometry.");
        //write out the boxes only
        for (Entity b : entities) {
            if (b.tag == Entity.EntityTag.NORMAL) {
                b.pack(output);
            }
        }

        //fixed length message, so zero pad if we don't have 25 boxes
        for (int i = (25 - entities.size()); i >= 0; i--) {
            Box.writeEmpty(output);
        }

        output.flush();

        ArrayList<Integer> raw = output.getBytes();
        //remove the first three codepoints...
        //they are the hash ray signal and space
        //that identify a thrifty tracer chunk

        String out = output.getUTF8();
        System.out.println("UTF8 Export string:" + out);

        reset();

        //load the data onto the clipoard
        //only do for standalone because of security issues



        //tweets are 140 unicode character points, which we are treating
        //as UTF-16, so give them 16 bits each (we won't be able to use)
        //all 16 bits, though
        byte[] bb = new byte[280];

        /*
        bb[0] = 0x00;
        bb[1] = 0x23;
        bb[2] = 0x26;
        bb[3] = 0x08;
        bb[4] = 0x20;
        bb[5] = 0x08;
         */
        //encode the ints into a unicode string
        for (int i = 0; i < raw.size(); i++) {
            int uc = raw.get(i);

            bb[2 * i] = (byte) (uc >> 8);
            bb[2 * i + 1] = (byte) (uc & 0xff);

        }
        String tmp = "";

        try {
            tmp = new String(bb, "UTF-16");
            System.out.println("UTF16 export: " + tmp);
        } catch (Exception e) {
            System.out.println("Export exception:" + e);
        }
        if (!online) {

            StringSelection ss = new StringSelection(tmp);
            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
        }


        //loopback for debugging, should replace scene, but nothing should change
        loadArray(raw);

        if (online) {
            try {
                getAppletContext().showDocument(new URL("javascript:doAlert(\"" + out + "\")"));
            } catch (Exception me) {
                System.out.println("Javascript exception:" + me);
            }
        }

        return tmp;
    }

    public String UTFEncode(int offset) {
        int lowBits = (offset & 0x3f) + 0x80;
        int midBits = ((offset & 0x7c0) >> 6) + 0x80;
        int highBits = ((offset & 0xf000) >> 12) + 0xe0;
        String hex = Integer.toHexString(highBits)
                + Integer.toHexString(midBits)
                + Integer.toHexString(lowBits);
        //URL encode
        String out = "&#x";
        for (int i = 0; i < hex.length(); i += 2) {
            // out += "%" + hex.charAt(i) + hex.charAt(i+1);
            out += hex.charAt(i) + "" + hex.charAt(i + 1);
        }
        out += ";";
        return out;
    }

    public void keyReleased() {
        if (keyCode == SHIFT) {
            isShiftDown = false;
        } else if (keyCode == CONTROL) {
            isCtrlDown = false;
        }
        // mouseMode = NONE;
        // cam.setMouseMode(LEFT, SGCamera.ORBIT);
    }

    private Entity findNearest() {

        /*
        //render scene to an offscreen buffer
        background(255);
        cam.feed();
        noLights();
        for (Entity b : entities) {
        b.pickDraw((PGraphics3D) g, color(entities.indexOf(b), 0, 0));
        }
        int which = (int)red(get(mouseX, mouseY));
        if (which == 255){
        draw();
        return null;
        }
        else{
        Entity ret = entities.get(which);
        draw();
        return ret;
        }
         */

        //loop through all of the boxes
        //map their centers into screen space
        //get the distance from screen space center
        //to the location of the mouse click
        //use the smallest one
        float sx = 0;
        float sy = 0;


        float minDist = 999999;
        float dist;
        Entity nearest = null;

        for (Entity b : entities) {
            sx = screenX(b.tx, b.ty, b.tz);
            sy = screenY(b.tx, b.ty, b.tz);
            dist = (sx - mouseX) * (sx - mouseX) + (sy - mouseY) * (sy - mouseY);

            if (dist < minDist) {
                minDist = dist;
                nearest = b;
            }
        }


        return nearest;


    }

    private void setCurrentShader(int current) {
        currentShader = current;
        //
        // redSlider.val = shaders[currentShader].fillColor.x;
        //  greenSlider.val = shaders[currentShader].fillColor.y;
        //  blueSlider.val = shaders[currentShader].fillColor.z;
        //   emitSlider.val = shaders[currentShader].emit;
        //shinySlider.val = shaders[currentShader].brdf.getN();
        //specularSlider.val = shaders[currentShader].brdf.getKs();

    }

    void getColor(int x, int y, int ID) {

        try {
            //color samples will be stored in this integrator
            PVector accum = new PVector();


            int samples = (int) (overSamples * overSamples);

            float[][] stratSamples = CoreFunctions.get2DSamples(overSamples);
            float[][] DOFSamples = CoreFunctions.get2DSamples(overSamples);

            float ax, ay;

            for (int s = 0; s < samples; s++) {

                ax = x + stratSamples[s][0] - .5f;
                ay = y + stratSamples[s][1] - .5f;

                PVector direction = cam.getRay(ax, ay);

                //prepare the picking ray
                Ray pick = new Ray(cam.pos, direction);

                //let's play with depth of field
                PVector focalPoint = PVector.add(cam.pos, PVector.mult(direction, cam.focus));
                PVector jpos = cam.pos.get();


                float r = lensRadius.getVal() * sqrt(DOFSamples[s][0]);
                float theta = 2 * PI * DOFSamples[s][1];

                float fx = r * cos(theta);
                float fy = r * sin(theta);


                jpos.add(PVector.mult(cam.up, fy));
                jpos.add(PVector.mult(cam.right, fx));
                focalPoint.sub(jpos);
                focalPoint.normalize();
                pick.origin = jpos;
                pick.direction = focalPoint;


                integrate(pick, 0, accum, ID);


                if (accum.x < 20 && (!Float.isNaN(accum.x))
                        && accum.y < 20 && (!Float.isNaN(accum.y))
                        && accum.z < 20 && (!Float.isNaN(accum.z))) {


                    film.addWeighted(x, y,
                            stratSamples[s][0] - .5f, stratSamples[s][1] - .5f,
                            accum, 1.f);
                } else {
                    System.out.println("Error! " + accum);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void integrate(Ray pick, int pathLength, PVector accum, int ID) {


        tracer.accelerator.findNearest(pick, ID);
        Intersectable tritemp;

        //if ray intersects anything, get the incident light
        if (pick.occluder != null) {

            tritemp = pick.occluder;

            if (pathLength == 0 || pick.bounceType == Ray.SPECULAR) {
                accum.add(PVector.mult(tritemp.shader.fillColor, tritemp.shader.emit));
            }

            directLighting(pick, tritemp, accum, ID);

            //sample the BRDF
            if (pathLength <= 4) {
                float rr = 1.f;
                //russian roulette on higher bounces
                if (pathLength > 1) {
                    rr = 2.f;
                    if (Math.random() < .5) {
                        rr = 0;
                    }
                }
                if (rr != 0) {

                    //indirect light

                    int sqrtSamples;
                    if (pathLength < 1) {
                        sqrtSamples = 2;
                    } else if (pathLength < 2) {
                        sqrtSamples = 2;
                    } else {
                        sqrtSamples = 2;
                    }


                    int numSamples = sqrtSamples * sqrtSamples;

                    float[][] stratSamples = CoreFunctions.get2DSamples(sqrtSamples);

                    PVector estimatedRadiance = new PVector();
                    PVector brdf = new PVector();

                    for (int s = 0; s < numSamples; s++) {

                        float pdf = 0;
                        brdf.set(0, 0, 0);

                        //zero out this accumulater, it should stil have values from the last pass
                        estimatedRadiance.set(0, 0, 0);


                        Ray incoming = new Ray();

                        incoming.origin = pick.project(pick.distance);
                        incoming.direction = new PVector();

                        PVector outgoing = pick.direction.get();
                        outgoing.mult(-1.f);



                        outgoing = pick.worldToLocal(outgoing);

                        //get a direction from the material
                        pdf = tritemp.shader.sampleReflectance(outgoing, incoming, null, brdf, pick.tu, pick.tv, null, null, stratSamples[s][0], stratSamples[s][1]);

                        if ((brdf.x > 0 || brdf.y > 0 || brdf.z > 0)
                                && (pdf > 0f)) {
                            //convert to the shading coordinate system.

                            incoming.direction = pick.localToWorld(incoming.direction);
                            incoming.minDist = .001f;

                            integrate(incoming, pathLength + 1, estimatedRadiance, ID);

                            //apply shader color
                            estimatedRadiance.mult(brdf);

                            estimatedRadiance.mult(rr / pdf);

                            estimatedRadiance.mult(1.f / numSamples);

                            accum.add(estimatedRadiance);

                        }
                    }

                }

            }

        } else {
            PVector ret = new PVector();
            back.getColor(pick.direction, ret);
            accum.add(ret);
        }



    }

    private void UTFWriteTest() {
        UTFWriter out = new UTFWriter();
        out.writeBits(4, 6);
        out.writeBits(20, 6);
        out.writeBits(28, 6);

        String UTFString = out.getUTF8();

        System.out.println("Test String is:" + UTFString);





    }

    private void UTFReadTest() {
        int[] input = new int[4];
        input[0] = 0x0504;
        input[1] = 0x3907;
        input[2] = 0x58;
        input[3] = 0x0;

        UTFReader utr = new UTFReader();




        for (int i = 0; i < input.length; i++) {
            utr.addPoint(input[i]);
        }

        //pop off the first 8 bits... should be 0
        int first = utr.getBits(6);
        System.out.println("test bit: " + first);
        first = utr.getBits(6);
        System.out.println("test bit: " + first);
        first = utr.getBits(6);
        System.out.println("test bit: " + first);
        first = utr.getBits(6);
        System.out.println("test bit: " + first);
        first = utr.getBits(6);
        System.out.println("test bit: " + first);
        first = utr.getBits(6);
        System.out.println("test bit: " + first);




    }

    public void loadString(String input) {
        loadString(input, false, true);
    }

    private void loadScene(UTFReader reader, boolean importScene) {





        version = reader.getBits(4);
        System.out.println("Version " + version + " found.");


        //read the camera

        int x = reader.getBits(8);

        x *= 8;
        x -= 1024;


        int y = reader.getBits(8);
        y *= 8;
        y -= 1024;
        //cam.pos.y = y;

        int z = reader.getBits(8);
        z *= 8;
        z -= 1024;
        //cam.pos.z = z;

        cam.setDesiredPosition(x, y, z);

        float rot = reader.getBits(5);
        lensRadius.setVal(reader.getBits(3));
        System.out.println("lens radius: " + lensRadius.getVal());
        focusPoint.setVal(reader.getBits(3));
        fieldOfView.setVal(reader.getBits(2));

        groundRad.setVal(reader.getBits(2));

        Backgrounds bg = Backgrounds.values()[reader.getBits(5)];
        back = BackgroundFactory.getBackGround(bg, this);

        //read in the shaders
        for (int i = 0; i < 11; i++) {
            //cast the int into an enum so we don't get our stremas crossed
            //naming materials
            int type = reader.getBits(5);
            Materials mat = Materials.values()[type];

            System.out.println("Material: " + mat);

            if (mat == Materials.MATTE) {

                shaders[i] = new MatteShader(reader);

            } else if (mat == Materials.PLASTIC) {
                shaders[i] = new ShinyPlasticShader(reader);
            } else if (mat == Materials.BLACKSTRIPES) {
                shaders[i] = new BlackStripeShader(reader);

            } else {
                System.out.println("unknown material found");
            }

        }

        //clear
        reset();

        // setEarthRad();




        int[] readInts = new int[20];


        for (int i = 0; i
                < 25; i++) {

            //we store these temporarily becasue we need to parse them to determine if its a box or sphere
            //translation
            readInts[0] = reader.getBits(8);
            readInts[1] = reader.getBits(8);
            readInts[2] = reader.getBits(8);

            //scale
            readInts[3] = reader.getBits(7);
            readInts[4] = reader.getBits(7);
            readInts[5] = reader.getBits(7);

            //rotation
            readInts[6] = reader.getBits(6);
            readInts[7] = reader.getBits(6);
            readInts[8] = reader.getBits(6);

            //shader
            readInts[9] = reader.getBits(4);

            //group
            readInts[10] = reader.getBits(2);

            Entity b;

            //sphere radius is stored in sx, with syz and sz zeroed
            if ((readInts[3] != 0) && (readInts[4] == 0)) {
                b = new ThriftySphere(readInts[0], readInts[1], readInts[2],
                        readInts[3], shaders, readInts[9]);
                b.setGroup(readInts[10]);
            } else {
                b = new Box(readInts[0], readInts[1], readInts[2],
                        readInts[3], readInts[4], readInts[5],
                        readInts[6], readInts[7], readInts[8],
                        shaders, readInts[9]);
                b.setGroup(readInts[10]);
            }
            //if the scale is zero, then this is an empty box and we are done parsing boxes

            if (readInts[3] != 0) {
                entities.add(b);
            } else {
                break;
            }




        }
    }

    public void loadArray(ArrayList<Integer> input) {
        //System.out.println(Math.random());
        UTFReader reader = new UTFReader();
        for (int i = 0; i < input.size(); i++) {

            reader.addPoint(input.get(i) - 0x51cf);

        }

        loadScene(reader, false);
    }

    public void loadString(String input, boolean importScene, boolean reset) {

        if (reset) {
            reset();
        }

        UTFReader reader = new UTFReader();


        try {

            System.out.println("javascript sent in:" + input);
            String output = URLDecoder.decode(input, "UTF-8");
            System.out.println("javascript decoded:" + output);


            //we dont' have any specific file identifiers anymore... no room
            /*
            if (output.codePointAt(0) == 0x23
            || output.codePointAt(1) == 0x2608) {
            System.out.println("found hash!");
            } else {
            System.out.println("Error! Does not seem to be a thriftyfile!");
            
            }
             * 
             * 
             */
            for (int i = 0; i < output.length(); i++) {

                reader.addPoint(output.codePointAt(i) - 0x51cf);

            }
        } catch (UnsupportedEncodingException ex) {
            System.out.println(ex);

        }

        loadScene(reader, importScene);
    }

    private void updateCanvas() {
        //calculate world luminance value, which is based on equation (1)
        //of the Reinhard tonemapping paper. Which is wrong.
        //stop calculating it after a few iterations because the average luminance
        //will have stabilized and we don't want to spend too much time 
        //reaveraging pixels
        if (film.autoBright && iterations < 2) {
            float maxBright = 0f;
            int N = 0;
            float t;
            for (int i = 0; i < sizeX; i++) {
                for (int j = 0; j < sizeY; j++) {
                    t = film.getLuminance(i, j);
                    //luminance returns -1 if the pixel hasn't been traced yet
                    if (t != -1) {
                        maxBright += (float) Math.log(t + .001f);
                        N++;
                    }

                }
            }

            maxBright /= N;
            maxBright = (float) Math.exp(maxBright);
            System.out.println("Maxbright is:" + maxBright);
            film.brightest = maxBright;

        }
        for (int i = 0; i < sizeX; i++) {
            for (int j = 0; j < sizeY; j++) {
                rendered.pixels[j * width + i] = film.toneMap(i, j);
            }
        }
    }

    private PVector getSelectedCentroid() {
        PVector toRet = new PVector();
        for (Entity b : selected) {
            toRet.add(b.tx, b.ty, b.tz);

        }

        toRet.div(selected.size());

        return toRet;

    }

    private void drawManipulators() {
        /*
        
        PVector center = getSelectedCentroid();
        
        stroke(255);
        
        PVector offset = new PVector();
        
        switch (currentCoordinateSystem) {
        case GLOBAL:
        switch (currentAxis) {
        case X_AXIS:
        offset.x += 50;
        break;
        case Y_AXIS:
        offset.y -= 50;
        break;
        case Z_AXIS:
        offset.z += 50;
        break;
        
        }
        break;
        case LOCAL:
        
        
        Entity first = selected.get(0);
        switch (currentAxis) {
        case X_AXIS:
        offset = first.getLocalXAxis(true);
        offset.mult(50);
        break;
        case Y_AXIS:
        offset = first.getLocalYAxis(true);
        offset.mult(-50);
        break;
        case Z_AXIS:
        offset = first.getLocalZAxis(true);
        offset.mult(50);
        break;
        }
        break;
        
        }
        
        line(center.x, center.y, center.z, center.x + offset.x, center.y + offset.y, center.z + offset.z);
        pushMatrix();
        translate(center.x, center.y, center.z);
        translate(offset.x, offset.y, offset.z);
        noStroke();
        sphere(10);
        popMatrix();
         */
    }

    private void loadRender() {
        //get location for saved file
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                try {
                    JFileChooser fc = new JFileChooser(new File("c:\\temp\\"));
                    int returnVal = fc.showOpenDialog(null);
                    if (returnVal == JFileChooser.APPROVE_OPTION) {
                        File selFile = fc.getSelectedFile();
                        if (selFile != null) {

                            String outName = selFile.getAbsolutePath();
                            loadRendering(outName);

                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private void loadRendering(String name) {
        mode = IMPORTING;
        Image image = null;

        try {

            // Read from an input stream
            InputStream is = new BufferedInputStream(
                    new FileInputStream(name));
            image = ImageIO.read(is);

            rendered = loadImageSync(image);

            is.close();
            String readdesc = stegoRead();

            loadString(readdesc);

            importOverlay = 256;
            mode = INTERACTIVE;

        } catch (IOException e) {
            System.out.println(e);

        }



    }

    public void dragEnter(DropTargetDragEvent dtde) {
        dtde.acceptDrag(DnDConstants.ACTION_COPY);
    }

    public void dragOver(DropTargetDragEvent dtde) {
        dtde.acceptDrag(DnDConstants.ACTION_COPY);
    }

    public void dropActionChanged(DropTargetDragEvent dtde) {
    }

    public void dragExit(DropTargetEvent dte) {
    }

    public void drop(DropTargetDropEvent dtde) {
        dtde.acceptDrop(DnDConstants.ACTION_COPY);
        Transferable transferable = dtde.getTransferable();
        DataFlavor flavors[] = transferable.getTransferDataFlavors();
        int successful = 0;

        for (int i = 0; i < flavors.length; i++) {
            try {
                Object stuff = transferable.getTransferData(flavors[i]);
                if (!(stuff instanceof java.util.List)) {
                    continue;
                }
                java.util.List list = (java.util.List) stuff;

                for (int j = 0; j < list.size(); j++) {
                    Object item = list.get(j);
                    if (item instanceof File) {
                        File file = (File) item;


                        String filename = file.getPath();
                        System.out.println("got file:" + filename);
                        loadRendering(filename);
                    } else {
                        System.out.println("got instance of " + item.toString());
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();










            }
        }
    }

    private void selectArea(PVector upperLeft, PVector lowerRight) {
        System.out.println("picking area");
        //clear out the old selection
        if (!isShiftDown) {
            for (Entity bx : entities) {
                bx.setSelected(false);
            }
            selected = new ArrayList();
        }

        float sx, sy;

        for (Entity b : entities) {
            sx = screenX(b.tx, b.ty, b.tz);
            sy = screenY(b.tx, b.ty, b.tz);

            if (sx > upperLeft.x && sx < lowerRight.x) {
                if (sy > upperLeft.y && sy < lowerRight.y) {
                    b.setSelected(true);
                    if (b.isSelected == false) {
                        //select this item
                        b.setSelected(true);
                        selected.add(b);
                        setCurrentShader(b.shaderIndex);

                        //if this is part of a group, select the rest of the group
                        if (b.getGroup() != 0) {
                            for (Entity e : entities) {
                                if (e.getGroup() == b.getGroup() && e.isSelected == false) {
                                    e.setSelected(true);
                                    selected.add(e);
                                }
                            }
                        }
                    }
                    updateMaterialEditor(shaders[b.shaderIndex]);
                }
            }

        }

    }

    class RenderThread extends Thread {

        public int ID;

        public RenderThread(int ID) {
            this.ID = ID;
        }

        // We must implement run, this gets triggered by start()
        public void run() {
            System.out.println("Running thread " + ID);
            //if more buckets, get one
            while (mode == RENDERING) {

                if (fixingBuckets == false) {

                    if (buckets.isEmpty()) {
                        fixingBuckets = true;
                        createBuckets();
                        fixingBuckets = false;
                    } //get the next bucket from the queque


                    RenderBucket bucket = (RenderBucket) buckets.remove(0);

                    // assert(bucket != null);

                    for (int i = bucket.minX; i < bucket.maxX; i++) {
                        for (int j = bucket.minY; j < bucket.maxY; j++) {
                            getColor(i, j, ID);
                        }

                    }
                } //so... another thread is building a new set of buckets to render
                //this thread will hang until the other thread is done.
                else {
                    try {
                        sleep(1);
                    } catch (Exception e) {
                        System.out.println(e);
                    }
                }
            }

            System.out.println("Thread " + ID + " exiting");
        }
    }

    static public void main(String args[]) {

        String test = "";

        test += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<upload><image><name/><title/><caption/><hash>p70uU</hash><deletehash>ZOJMBIUgzXvVC3b</deletehash><datetime>2011-09-05 03:56:09</datetime><type>image/png</type><animated>false</animated><width>550</width><height>550</height><size>369723</size><views>0</views><bandwidth>0</bandwidth></image><links><original>http://i.imgur.com/p70uU.png</original><imgur_page>http://imgur.com/p70uU</imgur_page><delete_page>http://imgur.com/delete/ZOJMBIUgzXvVC3b</delete_page><small_square>http://i.imgur.com/p70uUs.jpg</small_square><large_thumbnail>http://i.imgur.com/p70uUl.jpg</large_thumbnail></links></upload>";


        PApplet.main(new String[]{"TweetRay.TweetRay"});
    }
}
